#include "Variant.hpp"
#include "SyntaxTree.hpp"
#include <cstdio>
#include <cmath>

namespace Welp
{
  Variant::Variant()
  {
    Clear();
  }

  Variant::Variant(VariantType::Enum type)
  {
    Clear();
    mType = type;

    if (type == VariantType::Table)
      mTable = make_shared<Table>();
  }
  
  Variant::Variant(double value)
  {
    Clear();
    mType = VariantType::Number;
    mNumber = value;
  }

  Variant::Variant(void* value)
  {
    Clear();
    mType = VariantType::Pointer;
    mPointer = value;
  }

  Variant::Variant(const string& value)
  {
    Clear();
    mType = VariantType::String;
    mString = value;
  }

  Variant::Variant(const char* value)
  {
    Clear();
    mType = VariantType::String;
    mString = value;
  }

  Variant::Variant(VariantMethod function, Variant* self)
  {
    Clear();
    mType = VariantType::Function;
    mFunction = function;

    if (self != nullptr)
    {
      if (self->mType == VariantType::Pointer)
      {
        mPointer = self->mPointer;
      }
      else if (self->mType == VariantType::Table)
      {
        mTable = self->mTable;
      }
    }
  }
  
  Variant::Variant(FunctionNode* functionNode, Variant* self)
  {
    Clear();
    mType = VariantType::Function;
    mFunctionNode = functionNode;

    if (self != nullptr)
    {
      if (self->mType == VariantType::Pointer)
      {
        mPointer = self->mPointer;
      }
      else if (self->mType == VariantType::Table)
      {
        mTable = self->mTable;
      }
    }
  }

  void Variant::Clear()
  {
    mType = VariantType::Null;
    mNumber = 0.0;
    mPointer = nullptr;
    mFunction = nullptr;
    mTable = nullptr;
    mString.clear();
  }
  
  bool Variant::AsBool(const Location* location) const
  {
    switch (mType)
    {
      case VariantType::Null:
        return false;

      case VariantType::Number:
        return mNumber != 0.0;

      case VariantType::Pointer:
        return mPointer != nullptr;

      default:
        throw Exception(Format("A variant of type '%s' cannot be converted to a bool", TypeToString().c_str()), ExceptionType::Runtime, location);
    }
  }

  Variant Variant::Add(const Variant& lhs, const Variant& rhs, const Location* location)
  {
    // If we have two numbers...
    if (lhs.mType == VariantType::Number && rhs.mType == VariantType::Number)
    {
      return Variant(lhs.mNumber + rhs.mNumber);
    }
    else
    {
      throw Exception("Attempted to add two types that aren't both numbers", ExceptionType::Runtime, location);
    }
  }

  Variant Variant::Subtract(const Variant& lhs, const Variant& rhs, const Location* location)
  {
    // If we have two numbers...
    if (lhs.mType == VariantType::Number && rhs.mType == VariantType::Number)
    {
      return Variant(lhs.mNumber - rhs.mNumber);
    }
    else
    {
      throw Exception("Attempted to subtract two types that aren't both numbers", ExceptionType::Runtime, location);
    }
  }

  Variant Variant::Multiply(const Variant& lhs, const Variant& rhs, const Location* location)
  {
    // If we have two numbers...
    if (lhs.mType == VariantType::Number && rhs.mType == VariantType::Number)
    {
      return Variant(lhs.mNumber * rhs.mNumber);
    }
    else
    {
      throw Exception("Attempted to multiply two types that aren't both numbers", ExceptionType::Runtime, location);
    }
  }

  Variant Variant::Divide(const Variant& lhs, const Variant& rhs, const Location* location)
  {
    // If we have two numbers...
    if (lhs.mType == VariantType::Number && rhs.mType == VariantType::Number)
    {
      if (rhs.mNumber == 0.0)
      {
        throw Exception("Attempted to divide by zero", ExceptionType::Runtime, location);
      }
      
      return Variant(lhs.mNumber / rhs.mNumber);
    }
    else
    {
      throw Exception("Attempted to divide two types that aren't both numbers", ExceptionType::Runtime, location);
    }
  }

  Variant Variant::Modulo(const Variant& lhs, const Variant& rhs, const Location* location)
  {
    // If we have two numbers...
    if (lhs.mType == VariantType::Number && rhs.mType == VariantType::Number)
    {
      if (rhs.mNumber == 0.0)
      {
        throw Exception("Attempted to modulo by zero", ExceptionType::Runtime, location);
      }
      
      return Variant(fmod(lhs.mNumber, rhs.mNumber));
    }
    else
    {
      throw Exception("Attempted to modulo two types that aren't both numbers", ExceptionType::Runtime, location);
    }
  }

  Variant Variant::Exponent(const Variant& lhs, const Variant& rhs, const Location* location)
  {
    // If we have two numbers...
    if (lhs.mType == VariantType::Number && rhs.mType == VariantType::Number)
    {
      if (lhs.mNumber == 0.0)
      {
        // 0 ^ n where n < 0 is undefined
        if (rhs.mNumber < 0.0)
        {
          throw Exception("The exponent of 0 ^ n where n < 0 is undefined", ExceptionType::Runtime, location);
        }
        else if (rhs.mNumber == 0.0)
        {
          throw Exception("The exponent of 0 ^ 0 is indeterminate", ExceptionType::Runtime, location);
        }
      }

      return Variant(fmod(lhs.mNumber, rhs.mNumber));
    }
    else
    {
      throw Exception("Attempted to exponentiate two types that aren't both numbers", ExceptionType::Runtime, location);
    }
  }

  Variant Variant::Concatenate(const Variant& lhs, const Variant& rhs, const Location* location)
  {
    // Always results in a string...
    auto rhsString = rhs.ValueToString();
    auto lhsString = lhs.ValueToString();

    return Variant(lhsString + rhsString);
  }

  size_t VariantHasher::operator()(const Variant& variant) const
  {
    switch (variant.mType)
    {
    case VariantType::Null:
      return 0;

    case VariantType::Number:
      return hash<double>()(variant.mNumber) ^ 983249587;

    case VariantType::String:
      return hash<string>()(variant.mString) ^ 2012534005;

    case VariantType::Pointer:
      return hash<const void*>()(variant.mPointer) ^ 28923406;

    case VariantType::Function:
      return hash<const void*>()(variant.mFunction) ^ 98435 ^ hash<const void*>()(variant.mPointer) ^ 34621 ^ hash<const void*>()(variant.mTable.get());

    case VariantType::Table:
      return hash<const void*>()(variant.mTable.get()) ^ 1239;

    default:
      throw Exception("The variant is corrupted or has an invalid type set", ExceptionType::Internal, nullptr);
    }
  }

  bool VariantEqualTo::operator()(const Variant& a, const Variant& b) const
  {
    if (a.mType != b.mType)
      return false;

    switch (a.mType)
    {
    case VariantType::Null:
      return true; // All null types are equal

    case VariantType::Number:
      return a.mNumber == b.mNumber;

    case VariantType::String:
      return a.mString == b.mString;

    case VariantType::Pointer:
      return a.mPointer == b.mPointer;

    case VariantType::Function:
      return a.mFunction == b.mFunction && a.mTable == b.mTable && a.mPointer == b.mPointer;

    case VariantType::Table:
      return a.mTable == b.mTable;

    default:
      throw Exception("The variant is corrupted or has an invalid type set", ExceptionType::Internal, nullptr);
    }
  }

  string Variant::ValueToString() const
  {
    switch (mType)
    {
    case VariantType::Null:
      return "null";

    case VariantType::Number:
      return to_string((long double)mNumber);

    case VariantType::String:
      return mString;

    case VariantType::Pointer:
      return to_string((long long)mPointer);

    case VariantType::Function:
      return to_string((long long)mFunction);

    case VariantType::Table:
      return "<table>";

    default:
      throw Exception("The variant is corrupted or has an invalid type set", ExceptionType::Internal, nullptr);
    }
  }

  string Variant::TypeToString() const
  {
    switch (mType)
    {
    case VariantType::Null:
      return "Null";

    case VariantType::Number:
      return "Number";

    case VariantType::String:
      return "String";

    case VariantType::Pointer:
      return "Pointer";

    case VariantType::Function:
      return "Function";

    case VariantType::Table:
      return "Table";

    default:
      throw Exception("The variant is corrupted or has an invalid type set", ExceptionType::Internal, nullptr);
    }
  }

  Variant Variant::Call(VirtualMachine* vm, const vector<Variant>& arguments, const Location* location, Variant* localsTable)
  {
    if (mType != VariantType::Function && mType != VariantType::Table)
      throw Exception(Format("Only functions and tables are callable (attempted to call a '%s')", TypeToString().c_str()), ExceptionType::Runtime, location);

    if (mFunction != nullptr)
    {
      // User defined functions are simple to call (just expect arguments in order)
      return mFunction(*this, vm, arguments);
    }
    else if (mFunctionNode != nullptr)
    {
      // When calling a function node, we need to create a locals table, or in the case of a closure, use the passed in locals table

      Variant newLocals;
      if (!mFunctionNode->mIsClosure)
      {
        // Create a new table for the locals
        newLocals = Variant(VariantType::Table);
        localsTable = &newLocals;
      }

      size_t neededCount = mFunctionNode->mParameters.size();
      size_t givenCount = arguments.size();

      if (neededCount != givenCount)
      {
        throw Exception(Format("Attempting to call a function that takes %d parameter(s) with only %d argument(s)", neededCount, givenCount), ExceptionType::Runtime, location);
      }

      // Populate the locals table
      for (size_t i = 0; i < neededCount; ++i)
      {
        string& name = mFunctionNode->mParameters[i];
        const Variant& value = arguments[i];
        localsTable->InsertOverwrite(Variant(name), value, location);
      }
      
      try
      {
        ExecuteAll(mFunctionNode->mStatements, vm, localsTable);
      }
      catch (ReturnException e)
      {
        return e.mResult;
      }

      // If we got here, it means we never hit a return statement, so the return type is implicitly null
      return Variant();
    }
    else
    {
      throw Exception("Calling tables not yet suppored", ExceptionType::Internal, location);
    }
  }

  void Variant::InsertOverwrite(const Variant& key, const Variant& value, const Location* location)
  {
    if (mType != VariantType::Table)
      throw Exception("Attempting to insert a key and value into a variant that is not a table", ExceptionType::Runtime, location);

    (*mTable)[key] = value;
  }

  Variant& Variant::GetValue(const Variant& key, const Location* location)
  {
    if (mType != VariantType::Table)
      throw Exception("Attempting get a value by key from a variant that is not a table", ExceptionType::Runtime, location);

    auto valueIt = mTable->find(key);
    if (valueIt != mTable->end())
    {
      return valueIt->second;
    }
    else
    {
      throw Exception(Format("The key '%s' did not exist within the table", key.ValueToString().c_str()), ExceptionType::Runtime, location);
    }
  }

  Variant& Variant::GetValueOrDefault(const Variant& key, Variant& defaultValue, const Location* location)
  {
    if (mType != VariantType::Table)
      throw Exception("Attempting get a value by key from a variant that is not a table", ExceptionType::Runtime, location);

    auto valueIt = mTable->find(key);
    if (valueIt != mTable->end())
    {
      return valueIt->second;
    }
    else
    {
      return defaultValue;
    }
  }

  bool Variant::HasValue(const Variant& key, const Location* location)
  {
    if (mType != VariantType::Table)
      throw Exception("Attempting if a key exists on a variant that is not a table", ExceptionType::Runtime, location);

    return mTable->find(key) != mTable->end();
  }

  size_t Variant::GetCount(const Location* location)
  {
    if (mType != VariantType::Table)
      throw Exception("Attempting to get the number of elements of a variant that is not a table", ExceptionType::Runtime, location);

    return mTable->size();
  }
}
